package distributelock

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/go-redis/redis"
)

/*
分布式锁代码样例，要考虑的问题：
1. 锁的过期时间：如果业务处理时间过长，锁就会失效，导致其他客户端获取到锁，从而导致数据不一致
2. 锁的唯一标识：确保只有持有锁的客户端才能解锁，如果锁的唯一标识不一致，就会导致其他客户端解锁；
3. 锁的自动续期：如果业务处理时间过长，锁就会失效，导致其他客户端获取到锁，从而导致数据不一致
4. 锁的重试机制：如果获取锁失败，可以重试获取锁，直到获取到锁为止
5. 锁的可重入性：如果一个客户端已经获取到了锁，那么它可以再次获取锁，而不会导致死锁；
6. 锁的容错性：如果 Redis 集群出现了主从切换，或者某个节点出现了故障，锁仍然能够保证数据的一致性；
7. 客户端获取锁的顺序性：如果多个客户端同时尝试获取锁，应该按照一定的顺序获取锁，避免出现竞态条件；

针对上面注释的问题，我们可以通过以下方式解决：
1. 使用 Lua 脚本来确保加锁和解锁的原子性
2. 添加锁的唯一标识，确保只有持有锁的客户端才能解锁
3. 添加锁的自动续期机制，防止业务处理时间过长导致锁失效
*/

// Locker 可以通过它获取一把锁
type Locker struct {
	client          *redis.Client //redis客户端
	script          *redis.Script //解锁脚本
	ttl             time.Duration //锁的过期时间
	tryLockInterval time.Duration //重新获取锁的间隔
}

// Lock 不可重复使用
type Lock struct {
	client          *redis.Client //redis客户端
	script          *redis.Script //解锁脚本
	resource        string        //锁定的资源
	randomValue     string        //随机值，确保是锁的持有者自己释放锁
	watchDog        chan struct{} //看门狗，用于重置锁的过期时间
	ttl             time.Duration //锁的过期时间
	tryLockInterval time.Duration //重新获取锁的间隔
}

func NewLocker(client *redis.Client, ttl time.Duration, tryLockInterval time.Duration) *Locker {
	return &Locker{
		client:          client,
		script:          redis.NewScript(unlockScript),
		ttl:             ttl,
		tryLockInterval: tryLockInterval,
	}
}

// 获取锁, resource 锁定的资源，uuid 随机值，用于确保是锁的持有者自己释放锁
func (l *Locker) GetLock(resource string, uuid string) *Lock {
	return &Lock{
		client:          l.client,
		script:          l.script,
		resource:        resource,
		randomValue:     uuid,
		watchDog:        make(chan struct{}),
		ttl:             l.ttl,
		tryLockInterval: l.tryLockInterval,
	}
}

// 加锁
func (l *Lock) Lock(ctx context.Context) error {
	//预定义错误变量，减少内存分配
	var err error
	//尝试加锁
	err = l.TryLock()
	if err == nil {
		return nil
	}

	if !errors.Is(err, ErrLockFailed) {
		return err
	}

	//加锁失败，不断尝试获取锁
	ticker := time.NewTicker(l.tryLockInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done(): //超时
			fmt.Printf("resource: %s value: %s timeout\n", l.resource, l.randomValue)
			return ErrTimeout
		case <-ticker.C: //重新尝试加锁
			err := l.TryLock()
			if err == nil {
				return nil
			}
			if !errors.Is(err, ErrLockFailed) {
				return err
			}
		}
	}
}

// 尝试加锁
func (l *Lock) TryLock() error {
	//使用pipeline减少网络往返次数
	// pipeline := l.client.Pipeline()
	// setCmd := pipeline.SetNX(l.resource, l.randomValue, l.ttl)
	// _, err := pipeline.Exec()
	// if err != nil {
	// 	return err
	// }
	// //加锁失败
	// if !setCmd.Val() {
	// 	return ErrLockFailed
	// }

	//加锁
	ok, err := l.client.SetNX(l.resource, l.randomValue, l.ttl).Result()
	if err != nil {
		return err
	}

	//加锁失败
	if !ok {
		return ErrLockFailed
	}

	//加锁成功，启动看门狗
	go l.startWatchDog()
	fmt.Printf("resource: %s value: %s locked\n", l.resource, l.randomValue)
	return nil
}

// 启动看门狗
func (l *Lock) startWatchDog() {
	ticker := time.NewTicker(l.ttl / 3)
	defer ticker.Stop()

	for {
		select {
		//延长锁的过期时间
		case <-ticker.C:
			fmt.Printf("resource: %s value: %s delay expire\n", l.resource, l.randomValue)
			ok, err := l.client.Expire(l.resource, l.ttl).Result()
			if err != nil || !ok {
				return
			}

		//已经解锁
		case <-l.watchDog:
			fmt.Printf("resource: %s value: %s unlocked\n", l.resource, l.randomValue)
			return
		}
	}
}

// 解锁
func (l *Lock) Unlock(ctx context.Context) error {
	//关闭看门狗
	close(l.watchDog)

	//使用解锁脚本解锁（此命令低效，留存对比）
	// result, err := l.client.Eval(unlockScript, []string{l.resource}, l.randomValue).Result()
	// if err != nil {
	// 	return err
	// }

	//使用预加载的脚本减少每次执行的解析开销
	result, err := l.script.Run(l.client, []string{l.resource}, l.randomValue).Result()
	if err != nil {
		return err
	}

	if result.(int64) == 0 {
		return errors.New("unlock failed: key not exists or value not match")
	}

	return err
}
